package com.kuaimacode.kframework.api.service;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.dypnsapi.model.v20170525.GetMobileRequest;
import com.aliyuncs.dypnsapi.model.v20170525.GetMobileResponse;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.profile.DefaultProfile;
import com.kuaimacode.kframework.api.vo.request.AccessoLoginReq;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.*;

/**
 * @author qinyong
 * @Title:
 * @Package com.kuaimacode.kframework.api.service
 * @Description: created by IntelliJ IDEA
 * @date 2019-07-03 20:36
 */
@Service
public class AliTokenService {

    private static final Logger LOGGER = LoggerFactory.getLogger(AliTokenService.class);

    private final static String TIME_ZONE = "GMT";
    private final static String FORMAT_ISO8601 = "yyyy-MM-dd'T'HH:mm:ss'Z'";
    private final static String URL_ENCODING = "UTF-8";
    private static final String ALGORITHM_NAME = "HmacSHA1";
    private static final String ENCODING = "UTF-8";
    private static volatile String token = null;
    private static volatile long expireTime = 0;

    @Value("${aliyuns.api.keyid}")
    private String accessKeyId;
    @Value("${aliyuns.api.secret}")
    private String accessKeySecret;

    /**
     * 获取时间戳
     * 必须符合ISO8601规范，并需要使用UTC时间，时区为+0
     */
    public static String getISO8601Time(Date date) {
        Date nowDate = date;
        if (null == date) {
            nowDate = new Date();
        }
        SimpleDateFormat df = new SimpleDateFormat(FORMAT_ISO8601);
        df.setTimeZone(new SimpleTimeZone(0, TIME_ZONE));
        return df.format(nowDate);
    }

    /**
     * 获取UUID
     */
    public static String getUniqueNonce() {
        UUID uuid = UUID.randomUUID();
        return uuid.toString();
    }

    /**
     * URL编码
     * 使用UTF-8字符集按照 RFC3986 规则编码请求参数和参数取值
     */
    public static String percentEncode(String value) throws UnsupportedEncodingException {
        return value != null ? URLEncoder.encode(value, URL_ENCODING).replace("+", "%20")
                .replace("*", "%2A").replace("%7E", "~") : null;
    }

    /***
     * 将参数排序后，进行规范化设置，组合成请求字符串
     * @param queryParamsMap   所有请求参数
     * @return 规范化的请求字符串
     */
    public static String canonicalizedQuery(Map<String, String> queryParamsMap) {
        String[] sortedKeys = queryParamsMap.keySet().toArray(new String[]{});
        Arrays.sort(sortedKeys);
        String queryString = null;
        try {
            StringBuilder canonicalizedQueryString = new StringBuilder();
            for (String key : sortedKeys) {
                canonicalizedQueryString.append("&")
                        .append(percentEncode(key)).append("=")
                        .append(percentEncode(queryParamsMap.get(key)));
            }
            queryString = canonicalizedQueryString.toString().substring(1);
            LOGGER.info("规范化后的请求参数串：{}", queryString);
        } catch (UnsupportedEncodingException e) {
            LOGGER.error("UTF-8 encoding is not supported.", e);
        }
        return queryString;
    }

    /***
     * 构造签名字符串
     * @param method       HTTP请求的方法
     * @param urlPath      HTTP请求的资源路径
     * @param queryString  规范化的请求字符串
     * @return 签名字符串
     */
    public static String createStringToSign(String method, String urlPath, String queryString) {
        String stringToSign = null;
        try {
            StringBuilder strBuilderSign = new StringBuilder();
            strBuilderSign.append(method);
            strBuilderSign.append("&");
            strBuilderSign.append(percentEncode(urlPath));
            strBuilderSign.append("&");
            strBuilderSign.append(percentEncode(queryString));
            stringToSign = strBuilderSign.toString();
            LOGGER.info("构造的签名字符串：{}", stringToSign);
        } catch (UnsupportedEncodingException e) {
            LOGGER.error("UTF-8 encoding is not supported.", e);
        }
        return stringToSign;
    }

    /***
     * 计算签名
     * @param stringToSign      签名字符串
     * @param accessKeySecret   阿里云AccessKey Secret加上与号&
     * @return 计算得到的签名
     */
    public static String sign(String stringToSign, String accessKeySecret) {
        try {
            Mac mac = Mac.getInstance(ALGORITHM_NAME);
            mac.init(new SecretKeySpec(
                    accessKeySecret.getBytes(ENCODING),
                    ALGORITHM_NAME
            ));
            byte[] signData = mac.doFinal(stringToSign.getBytes(ENCODING));
            String signBase64 = DatatypeConverter.printBase64Binary(signData);
            LOGGER.info("计算的得到的签名：{}", signBase64);
            String signUrlEncode = percentEncode(signBase64);
            LOGGER.info("UrlEncode编码后的签名：{}", signUrlEncode);
            return signUrlEncode;
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalArgumentException(e.toString());
        } catch (UnsupportedEncodingException e) {
            throw new IllegalArgumentException(e.toString());
        } catch (InvalidKeyException e) {
            throw new IllegalArgumentException(e.toString());
        }
    }

    /***
     * 发送HTTP GET请求，获取token和有效期时间戳
     * @param queryString 请求参数
     */
    public static void processGETRequest(String queryString) {
        /**
         * 设置HTTP GET请求
         * 1. 使用HTTP协议
         * 2. Token服务域名：nls-meta.cn-shanghai.aliyuncs.com
         * 3. 请求路径：/
         * 4. 设置请求参数
         */
        String url = "http://nls-meta.cn-shanghai.aliyuncs.com";
        url = url + "/";
        url = url + "?" + queryString;
        LOGGER.info("HTTP请求链接：{}", url);
        Request request = new Request.Builder()
                .url(url)
                .header("Accept", "application/json")
                .get()
                .build();
        try {
            OkHttpClient client = new OkHttpClient();
            Response response = client.newCall(request).execute();
            String result = response.body().string();
            if (response.isSuccessful()) {
                JSONObject rootObj = JSON.parseObject(result);
                JSONObject tokenObj = rootObj.getJSONObject("Token");
                if (tokenObj != null) {
                    token = tokenObj.getString("Id");
                    expireTime = tokenObj.getLongValue("ExpireTime");
                } else {
                    System.err.println("提交获取Token请求失败: " + result);
                }
            } else {
                System.err.println("提交获取Token请求失败: " + result);
            }
            response.close();
        } catch (Exception e) {
            LOGGER.error("获取token失败", e);
        }
    }

    public String getToken() {
        if (token != null && expireTime > System.currentTimeMillis()/1000) {
            return token;
        }
        // 所有请求参数
        Map<String, String> queryParamsMap = new HashMap<>();
        queryParamsMap.put("AccessKeyId", accessKeyId);
        queryParamsMap.put("Action", "CreateToken");
        queryParamsMap.put("Version", "2019-02-28");
        queryParamsMap.put("Timestamp", getISO8601Time(null));
        queryParamsMap.put("Format", "JSON");
        queryParamsMap.put("RegionId", "cn-shanghai");
        queryParamsMap.put("SignatureMethod", "HMAC-SHA1");
        queryParamsMap.put("SignatureVersion", "1.0");
        queryParamsMap.put("SignatureNonce", getUniqueNonce());
        /**
         * 1.构造规范化的请求字符串
         */
        String queryString = canonicalizedQuery(queryParamsMap);
        if (null == queryString) {
            LOGGER.error("构造规范化的请求字符串失败！");
            return "";
        }
        /**
         * 2.构造签名字符串
         */
        String method = "GET";  // 发送请求的 HTTP 方法，GET
        String urlPath = "/";   // 请求路径
        String stringToSign = createStringToSign(method, urlPath, queryString);
        if (null == stringToSign) {
            LOGGER.error("构造签名字符串失败");
            return "";
        }
        /**
         * 3.计算签名
         */
        String signature = sign(stringToSign, accessKeySecret + "&");
        if (null == signature) {
            LOGGER.error("计算签名失败!");
            return "";
        }
        /**
         * 4.将签名加入到第1步获取的请求字符串
         */
        String queryStringWithSign = "Signature=" + signature + "&" + queryString;
        LOGGER.info("带有签名的请求字符串：{}", queryStringWithSign);
        /**
         * 5.发送HTTP GET请求，获取token
         */
        processGETRequest(queryStringWithSign);
        if (token != null) {
            LOGGER.info("获取的Token：{}, 有效期时间戳（秒）：{}", token, expireTime);
            // 将10位数的时间戳转换为北京时间
            String expireDate = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(expireTime * 1000));
            LOGGER.info("Token有效期的北京时间：{}", expireDate);
            return token;
        }
        return "";
    }

    public String getMobile(AccessoLoginReq req) {
        LOGGER.info("ali get mobile request: {}", req);
        DefaultProfile profile = DefaultProfile.getProfile("default", accessKeyId, accessKeySecret);
        IAcsClient client = new DefaultAcsClient(profile);

        GetMobileRequest request = new GetMobileRequest();
        request.setAccessToken(req.getToken());

        try {
            GetMobileResponse response = client.getAcsResponse(request);
            LOGGER.info("ali get mobile response: {}", JSONObject.toJSONString(response));
            if ("OK".equals(response.getCode())) {
                return response.getGetMobileResultDTO().getMobile();
            }
        } catch (ClientException e) {
            LOGGER.error("ali get mobile error, req: {}", req, e);
        }
        return null;
    }
}
